import json
import asyncio
from pathlib import Path
from time import time
from typing import Dict, List, Any

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse
import uvicorn
from pydantic import BaseModel

app = FastAPI()

# 房间号 -> 客户端列表
# 每个客户端结构：{"ws": WebSocket, "num": str, "money": str, ...}
rooms: Dict[str, List[Dict[str, Any]]] = {}

# 每个房间最近一次 get / set 的时间，避免全局互相覆盖
room_time: Dict[str, Dict[str, float]] = {}   # {room: {"get": 0.0, "set": 0.0}}


# ---------- 页面 ----------
@app.get("/", response_class=HTMLResponse)
def index():
    html_path = Path("templates/index.html")
    return HTMLResponse(html_path.read_text(encoding="utf-8")) if html_path.exists() \
        else HTMLResponse("<h1>Missing templates/index.html</h1>")


# ---------- 调试接口 ----------
@app.get("/getMap")
def get_map():
    """返回每个房间公开信息（去掉 ws 对象）"""
    return JSONResponse({
        room: [{k: v for k, v in cli.items() if k != "ws"} for cli in clients]
        for room, clients in rooms.items()
    })


# ---------- WebSocket ----------
import asyncio
import json
from fastapi import WebSocket, WebSocketDisconnect

PING_INTERVAL = 5          # 每 5 s 一次
MISS_PONG_LIMIT = 2        # 连续 2 次没回就踢掉
WRITE_TIMEOUT = 5      # 秒
xzTime = ""# 下注剩余时间
async def safe_send(ws: WebSocket, text: str, timeout: float = WRITE_TIMEOUT):
    try:
        await asyncio.wait_for(ws.send_text(text), timeout)
    except asyncio.TimeoutError:
        # 写超时 → 认为连接已死，主动关闭
        await ws.close(code=1001, reason="write timeout")
        raise RuntimeError("websocket send timeout") from None
# 1. 全局异步锁
rooms_lock = asyncio.Lock()
@app.websocket("/ws")
async def websocket_root(ws: WebSocket):
    await ws.accept()
    room: str = None
    num: str = None
    miss_pong = 0            # 漏 pong 计数器

    # 心跳协程：定时发 ping
    async def heartbeat():
        nonlocal miss_pong
        while miss_pong <= MISS_PONG_LIMIT:
            try:
                await safe_send(ws,json.dumps({"type": "ping"}))
                await asyncio.sleep(PING_INTERVAL)
            except Exception as e:
                print(e)
            finally:
                miss_pong += 1  # 发完就 +1，收到 pong 再清零
        try:
            print('close ws')
            await ws.close()
        except Exception as e:
            print(e)
    # 启动心跳任务
    hb_task = asyncio.create_task(heartbeat())

    try:
        while miss_pong <= MISS_PONG_LIMIT:
            await asyncio.sleep(0.1)
            raw = await ws.receive_text()
            data = json.loads(raw)
            # 处理业务消息
            if data.get("type") == "pong":
                miss_pong = 0            # 收到 pong，清零
                continue

            print(raw)
            # 第一次 init 消息里把 room / num 记下来
            if "init" in data and data.get("init") and "title" in data and "num" in data:
                room = data["title"]
                num = data["num"]

            await handle_message(ws, raw)
    except WebSocketDisconnect:
        print(f"{num} 的 ws 已结束")
    finally:
        print("清理ws")
        hb_task.cancel()                   # 停掉心跳
        # 加锁
        asyncio.Lock()
        # 清理掉线客户端
        if room and num:
            async with rooms_lock:
                clients = rooms.get(room, [])
                # 安全删除
                rooms[room] = [c for c in clients if c["num"] != num]
                print(clients,rooms[room])
                if not rooms[room]:
                    rooms.pop(room, None)
                    room_time.pop(room, None)


async def handle_message(ws: WebSocket, raw: str):
    global xzTime
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return
    room = data.get("title")
    if not room:
        return

    # 初始化
    if data.get("init"):
        num = data.get("num")
        async with rooms_lock:
            rooms.setdefault(room, [])
            rooms[room].append({
                "ws": ws,
                "num": num,
                "money": "未查询",
                "getMoneyTime": "未查询",
                "setMoneyTime": "未下注",
            })
            # 初始化房间时间记录
            room_time.setdefault(room, {"get": time(), "set": time()})
        return

    instr = data.get("instruction")
    if instr == 'getTime':
        xzTime = data.get("timeNum")
        return
    num = data.get("num")
    money = data.get("money")

    # 更新本地数据
    for cli in rooms.get(room, []):
        if cli["num"] == num:
            if instr == "get":
                cli["money"] = money
                cli["getMoneyTime"] = room_time[room]["get"] = time()
            elif instr == "set":
                cli["money"] = money
                cli["getMoneyTime"] = room_time[room]["set"] = time()
                cli["setMoneyTime"] = room_time[room]["set"] = time()
            break


# ---------- 广播 ----------
async def broadcast(room: str, payload: dict, nums = None):
    """向指定房间广播；nums 为空时发给所有人"""
    msg = json.dumps(payload, ensure_ascii=False)
    print(rooms.get(room, []))
    for cli in rooms.get(room, []):
        if nums and cli["num"] not in nums:
            continue
        try:
            await cli["ws"].send_text(msg)
        except Exception as e:
            print("send error:", e)
# 1. 定义 JSON 结构
class BetReq(BaseModel):
    map_key: str
class BetReq2(BaseModel):
    map_key: str
    text: str
    num: int
    money: str

# ---------- HTTP 触发 ----------
@app.post("/send/get")
async def send_get(req: BetReq):
    map_key = req.map_key
    print("/send/get")
    print(map_key)
    if not map_key:
        for room, clients in rooms.items():
            await broadcast(room, {"instruction": "get"})
        return JSONResponse({"ok": True})
    await broadcast(map_key, {"instruction": "get"})
    return JSONResponse({"ok": True})


@app.post("/send/set")
async def send_set(req: BetReq2):
    num = req.num
    map_key = req.map_key
    text = req.text
    money = req.money
    print("/send/set")
    print(map_key)
    await broadcast(map_key, {"instruction": "set", "text": text, "money": money},[num])
    return JSONResponse({"ok": True})

@app.post("/send/xzTime")
async def xzTime(req: BetReq):
    global xzTime
    map_key = req.map_key
    nums = rooms.get(map_key, [])
    xzTime = ""
    if nums.__len__()>0:
        await broadcast(map_key, {"instruction": "getTime"})
        # 等待5秒在返回
        await asyncio.sleep(4)
        return JSONResponse({"ok": True,"data":xzTime})

    return JSONResponse({"error": "该房间没有设备"})


# ---------- 启动 ----------
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8080, reload=False)